feat(types): derive and export the strict authoring twin of the node face - #8642
Conversation
…face Publish `StrictAnyComponentSchema`, `StrictSchemaNodeSchema` and `deriveStrictAuthoringSchema` from `@object-ui/types/zod` — a derived twin of the node face that closes every declared object at every depth. Additive: the rendering face keeps its `.passthrough()`, no accept set moves, and no consumer is wired to the new face. The twins are derived, never hand-written. Objects are cloned by patching a copy of their own def so `.refine()` / `.superRefine()` checks survive; a twin rebuilt with `z.object(shape)` would drop them and under-report red. Both sides of a pipe are walked, so a `z.preprocess`'s real schema is closed too. Part of #8345 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
The snippet used `ButtonSchema` without importing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…e population The walker's type guard was `typeof value === 'object'`. Zod 4.4.3 builds some objects through `$ZodObjectJIT`, whose instances are CALLABLE — 20 of them are reachable on this face, all through `@objectstack/spec`-derived subtrees. The guard handed each one back untouched, so the subtree beneath went unwalked and 6 objects stayed open on the twin: an invented key inside one of them was accepted and silently dropped, while the same key at the root was refused and named. Nothing could catch that. No document in the 556-document corpus carries an undeclared key inside those 6 objects, and the pin file's own census began `typeof node !== 'object'` — it shared the blind spot with what it measured. Both guards now admit callable nodes, and the pin that was missing is here: a POPULATION pin requiring every object reachable on the twin to carry `catchall: never`, with the function-typed count asserted non-zero so the control cannot silently regress. REPRO-A is pinned as a document too, and the cycle's real invariant — the barrel is the sole entry — is pinned in both of its halves. Part of #8345 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Contract review (
|
| reading | value |
|---|---|
| pins (a) known-good / (b) root key refused + named / (b) tolerant accepts / (c) 5-row tolerant table after forcing | true / false + ["inventedTop"] / true / all 5 rows unchanged |
child slot (recursion point live): invented child key refused + named; child's own variant/size accepted |
["inventedChild"]; true — R4 shape, not R2 |
StrictSchemaNodeSchema: bare string accepted / text + nope refused |
true / false |
corpus (scripts/measure-strict-authoring-face.mjs --json against my scratch dist) |
556 docs · 174 whole-tree strict · 46 red today · 2099 nodes · 179 refused (127 strict-only / 52 red) · 107 / 94 / 62 · collisions 0 · arms w/o literal type 0 — identical to every cell the PR quotes |
| plausibility vs card tables | card R4 = 176/553 @ 5505aec, R2 = 294/553; this head 174/556 sits on the R4 line, the R2 blow-up is gone. Controls present (red-today, collisions, arms-without-literal). |
opaque census (onOpaqueShape, one forced parse) |
custom 71 · function 4 · transform 1 = 76 |
ZodError on the strict path (#8498 cliff, closed by #8544) |
one invented leaf key at depth 0/1/2/4/6 → flat issues 1/12/23/45/67, JSON 111 B → 9,177 B, ≤0.5 ms/parse — linear, not the 25×/level cliff; cloneWithDef preserves the discriminator, so #8544's fix carries into the twin |
| tolerant face non-mutation (function-aware census) | closed objects reachable from AnyComponentSchema: 58 before and after forcing both twins |
| module cycle, Node on dist | deep-module-first: same object as barrel's, nested key refused; barrel-first: same object, export present. No load-time derivation (createStrictWalker() = one empty Map, two z.lazy wrappers). New edges: strict-authoring-face.ts ⇄ zod/index.zod.ts only — no edge from base.zod.ts (the refused getter-binding route is not taken; base.zod.ts is byte-identical to base). |
Findings
1. BLOCKER — the strict twin does not close every reachable object; 6 stay open, and an off-spec key inside them is silently accepted and dropped.
src/strict-authoring-face.ts:141-142: isZodType = (value) => typeof value === 'object' && value !== null && '_zod' in value. On this face 20 zod 4.4.3 schema nodes are typeof 'function' (def kinds: object 15, enum 3, record 1, pipe 1; they parse normally). The guard returns each one unwalked, so its whole subtree keeps the tolerant shape. Function-aware census of the forced StrictAnyComponentSchema: 302 objects, 296 closed, 6 open — page.interfaceConfig.sort[] (strip), page.slots.header[0].in.visibleWhen[1] + its .meta (strip), page.slots.header[0].in.dataSource.filter(lazy).right (strip), list-view.bulkActionDefs[].params[] and …params[].options[] (catchall: unknown, i.e. passthrough).
Reproducer: { type:'page', interfaceConfig:{ source:'x', sort:[{ field:'a', order:'asc', inventedDeepKey:1 }] } } → tolerant true, strict true, no unrecognized_keys, strict output {"field":"a","order":"asc"} (key dropped). Control, same document + inventedTopKey at root → strict false, ["inventedTopKey"] named.
Fix ablation (scratch copy, guard admits typeof value === 'function'): 300 objects / 300 closed / 0 open; reproducer → strict false, ["inventedDeepKey"]; known-good still true; list-view.…params[0].bogus now named alongside the pre-existing required-value issues. The prototype's guard (if (!schema?._zod) return schema;, script line 260) does not have this hole — the shipped walker regressed coverage relative to the prototype on exactly this class.
Expectation: admit function-typed nodes in isZodType; add a pin that walks the forced twin with a function-aware visitor and asserts every reachable object def has catchall.type === 'never' (with a non-vacuity assertion that ≥1 function-typed node was visited); add the reproducer above as a pin, both directions.
2. MAJOR — the published contract text is false at those six depths. Changeset: "refusing any undeclared key at any depth with an unrecognized_keys issue that names it"; README: "closes every declared object, at every depth"; PR body: "Every reachable object is closed". The changeset is the payload that ships. Expectation: make the text true (finding 1) — do not soften the text instead.
3. MINOR (latent) — the walker's default: arm swallows container def kinds silently and reports nothing. Synthetic probe: z.set(obj), z.map(k, obj), obj.prefault(…) → strict twin accepts {a:'x', bogus:1} inside them, inner objects 0 closed, and no onOpaqueShape call — so the "complete limit list = custom/function/transform" claim holds only for kinds the switch names. None of these kinds is on the face today (after the finding-1 fix the census reads 0 open), so no accept set moves now. Controls: catchall: obj and z.record(k, obj) are walked correctly (strict refuses). Expectation: add set/map/prefault/promise arms, or make default: report unknown non-leaf kinds through onOpaqueShape so the limit list is measured rather than enumerated by hand.
4. MINOR — the pin file's instrument shares the defect. closedObjectCount (test file, bottom) starts with typeof node !== 'object', so the "identical before and after" reading and the "39 closed" figure could not see the six open objects; the corpus is blind too (no document among the 556 carries an undeclared key inside those objects — base, head and the swapped-in shipped face all read 174 regardless). Expectation: function-aware counter; the pin in finding 1 is the control the file is missing.
5. INFO — the module cycle's real invariant is "the barrel is the sole entry", and nothing pins it. "Read only inside the lazy getters" is true of the shipped source (import at line 84, runtime reads only at 300/312) and is what makes both Node orders green — but rollup's synthesized namespace on a deep-first entry is the shape #8344 paid for, and the only thing keeping it unreachable is the exports map plus the fact that no file but the barrel imports strict-authoring-face. Build Docs on this head is the skip path (12 s, "No docs-related files changed") and says nothing about Turbopack. Expectation (non-blocking): a source-scan pin that no module other than zod/index.zod.ts imports strict-authoring-face.
6. INFO — check:node-esm-load is not a per-PR gate. node-esm-load-gate.yml triggers on schedule (17 4 * * *) and on push for its own two paths; only check:esm-specifiers runs per PR. So "cannot arise on CI" is true only because CI never runs the load leg on a PR. The provenance explanation is plausible and not this diff, but the PR should state the --force-build re-read rather than lean on CI.
7. INFO — placement (option A) and the census collision, reproduced. Unmutated: pin file 19/19 and parity file 32/32 under vitest. Appending one export const to zod/index.zod.ts → Tests 1 failed | 31 passed (32), failure naming index.zod.ts#__StrictAuthoringCensusProbe. zod-mirror-parity.test.ts is unchanged vs base (fence honoured); the zod-lazy-getter-identity-7918 row lives in its own test file, also unchanged. Seat ruled A on the card; nothing here is a maintainer item.
8. INFO — the claim comment on #8345 (5590024076) carries no Container & model: line. Tier stated: none. (The dispatch comment says "built at TIER_DEFAULT"; the claim itself does not carry the line.) Commit trailers: both commits carry Co-Authored-By + Claude-Session; commit 1 carries Part of #8345. RULE 2: satisfied.
Changeset verdict
.changeset/8345-strict-authoring-face.md: '@object-ui/types': minor — grade correct (additive export, nothing removed, no BREAKING). Text false at the six depths in finding 1; must be corrected with the fix, not reworded around it.
Tests
19 pins pass; no .skip/.only/.todo; both directions pinned (strict refuses root and child invented keys and names them; tolerant control accepts the same documents; five-row tolerant table after forcing); revert reddens (the exports do not exist at base). What is missing is the whole-graph closure pin (finding 1/4).
CI on ad99bee
33 check runs, all completed, none in progress: Type Check ✓ (19:33:19) · Build Docs ✓ (skip path, 12 s) · Bundle Analysis ✓ (types (strict-authoring-face.js) 11.72 KB / 4.48 KB gz; console eager closure 3477.4 KB under the 3512.7 KB budget) · Test (shard 1/4 … 4/4) all ✓ · Lint ✓ · Changeset Bump Policy / Declaration / Fixed Group / Overwrite ✓ · README Export Check ✓ · Governed Surface Queue Guard ✓. mergeable_state: unknown at read (draft; 8 commits behind origin/main 4dc80d0f).
Maintainer-only merge: no — non-governed additive export under a maintainer-ruled programme; every finding above is implementer-floor. Carrier needs:contract-review stays on PR and card until finding 1 is fixed and re-reviewed.
Generated by Claude Code
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
The `pipe` arm's comment said "one pipe reachable, `in` an array and `out` the transform". That reading was taken through the object-only guard. With callable nodes admitted, four pipes are reachable — including a preprocessor under `page.interfaceConfig.filterBy[]` that the old walker could not see at all. No code changes; the comment now states what the tree measures. Part of #8345 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review (
|
| reading | value |
|---|---|
| tolerant face, function-aware census (forced) | 4766 nodes · 20 callable (object 15, enum 3, record 1, pipe 1) · 300 objects · 58 closed / 242 open |
| strict twin, same census | 4998 nodes · 300 objects · 300 closed / 0 open |
REPRO-A {type:'page', interfaceConfig:{source:'x', sort:[{field:'a', order:'asc', inventedDeepKey:1}]}} |
tolerant true · strict false, unrecognized_keys = ["inventedDeepKey"] |
control: same doc + inventedTopKey at root |
strict false, ["inventedDeepKey","inventedTopKey"] |
| control: same doc without the key | strict true |
known-good card/button/text; root invented key |
strict true; strict false ["inventedTopLevelKey"] |
list-view.bulkActionDefs[].params[] / …options[] with a bogus key |
strict names bogusParamKey / bogusOptionKey (alongside pre-existing issues; these two were catchall: unknown at ad99bee) |
opaque census (onOpaqueShape, one forced parse) |
custom 71 · function 4 · transform 4 = 79 |
| pipes reachable on the face | 4: transform→enum (…interfaceConfig/…/filterBy/…/element/operator, the preprocessor), object→transform (…slots/…/header/…/opt0), string→transform (…header/…/in/visibleWhen/…), array→transform (…exportOptions/…) |
| guard ablation on a dist copy (object-only guard, walker otherwise head) | 302 objects · 296 closed · 6 open — …interfaceConfig/…/sort/…/element [strip], …header/…/in/visibleWhen/…/opt1 [strip], its …/meta/innerType [strip], …in/dataSource/…/filter/…/lazy/right [strip], …bulkActionDefs/…/params/…/element [unknown], …/options/…/element [unknown]; REPRO-A strict true, no keys; opaque back to 76 |
ablation E under vitest (src guard reverted, blob 269c59b6→ecf52cc1, restored to 269c59b6) |
exit 1, 2 failed / 23 passed: population pin expected [ …(6) ] to deeply equal [] + REPRO-A |
ablation F under vitest (pin census blinded, walker correct, blob 4f24ec93→d2ab2853, restored) |
exit 1, 1 failed / 24 passed: only "the census can see CALLABLE schema nodes" fires (expected 0 to be greater than 0); population assertion vacuously green — the control is doing its job |
corpus scripts/measure-strict-authoring-face.mjs --json, base c4326fe vs head |
556 / 174 / 46 / 2099 / 179 = 127+52 / 107·94·62 / 0·0 — identical in every cell; whole JSON identical except the scratch path inside one error string; script itself unchanged by the PR |
| cycle invariant, measured directly | exports map: 0 wildcard subpaths, 0 entries reaching strict-authoring-face; repo specifier scan (packages apps examples scripts e2e, excl. node_modules/dist): sole importer packages/types/src/zod/index.zod.ts |
default: arm probe (owed item 3) |
z.set(obj), z.map(k,obj), obj.prefault(…), z.promise(obj) → inner object 0/1 closed, strict twin accepts {a:'x', bogus:1}, 0 onOpaqueShape calls; controls z.record(k,obj) and .catchall(obj) → 1/1 closed, refused |
Findings
1. RESOLVED (was BLOCKER) — the twin closes every object reachable on the face. strict-authoring-face.ts:169 guard now value !== null && (typeof value === 'object' || typeof value === 'function') && '_zod' in value. Population 300/300 on my build; REPRO-A refused and named; root control unchanged; guard ablation reproduces the six open objects and the silent accept exactly; ablation E turns the new population pin and REPRO-A red. The callable nodes clone back object-typed (strict census functionTyped = 3, all enum leaves), which is representation, not behaviour — the pins read behaviour.
2. RESOLVED (was MAJOR) — changeset / README / body text is true as measured. "refusing any undeclared key at any depth", "closes every declared object, at every depth", "Every reachable object is closed": 0 open objects on the reachable graph. Text kept, not softened — the direction the previous review asked for. (The director's optional "cite the 300/300 census in the changeset" was not taken; keeping a drifting number out of the shipped payload is defensible, and the population pin is where the number lives.)
3. MINOR (latent) — NOT MET, and not acknowledged in the body or the os-dev-report: the default: arm still swallows set / map / prefault / promise. strict-authoring-face.ts:296-300 (default: out = schema; with no report). Measured above: inner objects unclosed and unreported, so for a consumer calling deriveStrictAuthoringSchema on a schema carrying one of these, "reported through onOpaqueShape rather than skipped" does not hold. No accept set moves today (none of these kinds is on the face: the tolerant census shows 0 such nodes, and the population pin's census() does descend valueType/innerType, so the first such container to reach the face with an object inside would turn the population pin red — the face is guarded; consumer-derived schemas are not). No pin covers the report path for these kinds (grep -nE "z\.set\(|z\.map\(|prefault|z\.promise" on the pin file: 0 hits). Expectation: add the four arms (set: valueType; map: keyType+valueType; promise/prefault: innerType) or make default: report unknown non-leaf kinds via onOpaqueShape; extend the three-kind control pin. Small enough to land here while the PR is draft; proportionate as a follow-up if the PM seat prefers — the director's re-hang precondition named items 1–4, and this one was re-hung short.
4. RESOLVED (was MINOR) — the pin's instrument admits callable nodes. isSchemaNode at the bottom of the pin file mirrors the walker's guard; functionTyped asserted > 0; ablation F shows that control is the only thing standing between a blinded census and a vacuous green.
5. RESOLVED (INFO) — cycle invariant pinned in both halves; false evidence sentences gone. Exports-map half and sole-importer scan are in the pin file (predicates re-measured independently above; the G/H mutations were not re-run by me). PR body: "cannot arise on CI" appears only inside a correction paragraph that retracts it; Build Docs green is not cited (on this head it is again the 12 s skip path, 20:40:40→20:40:52, and says nothing); check:node-esm-load --force-build is stated as the only reading of the load leg. The rollup deep-first RED is recorded as the reason the invariant matters.
6. INFO — NOT MET: the re-hang comments on #8345 (5591600708, 5591636881) carry no Container & model: line. Tier is fused from harness stamps by the seat; this is claim hygiene only. Commit trailers on both new commits: Part of #8345, Co-Authored-By, Claude-Session present.
7. INFO — NOT MET at read time: needs:contract-review is on the PR but NOT on the card. PR labels at read: documentation · package: types · tests · needs:contract-review. Card #8345 labels at read (GraphQL get_labels, 5 total): package: types · pm:dispatched · priority:p2 · pm:blocking · domain:spec — carrier absent; card updated_at 20:46:15Z, the same instant as comment 5591636881, which states "carrier present on both PR and card". That sentence does not match the label set I read. ⛔ This seat touches no labels; the PM seat decides the card side.
8. INFO — the corpus is confirmed structurally blind to finding 1, from the other side. Base = head in every cell and the guard ablation moves no cell either (the pipe/opaque counts move, the corpus does not). The body says so; recorded here so nobody cites the corpus as evidence of closure.
Owed item → status
| # | owed item (director 5591397819) |
status | evidence |
|---|---|---|---|
| 1 | BLOCKER: guard rejects callable nodes → 6 open; fix + reproducer pin | met | 300/300 closed on my build; REPRO-A refused, inventedDeepKey named; dist ablation → 302/296/6 + silent accept; ablation E → 2 failed |
| 2 | MAJOR: "every reachable object" text false at those depths | met | text unchanged, now true at 0 open; census not cited in changeset (optional half not taken) |
| 3 | MINOR: default: swallows set/map/prefault/promise — walk or report, extend control pin |
not met | probe: 0/1 closed, accepts bogus, 0 reports; no arm added, no pin, not mentioned in body or report |
| 4 | MINOR: pin's own counter blind | met | isSchemaNode admits functions; functionTyped > 0 control; ablation F fires only that control |
| 5 | INFO: pin "barrel is sole entry"; drop Build Docs / "cannot arise on CI" |
met | two pins present; exports map 0 wildcard / 0 deep entry; sole importer = barrel; body corrected |
| 6 | INFO: Container & model: on the re-hang comment |
not met | absent from 5591600708 and 5591636881 |
| 7 | re-hang needs:contract-review on both PR and card |
partly met | PR: present. Card: absent at read (5 labels, none is the carrier) |
CI on 8e9b563 (read once, 21:0x Z)
33 check runs, all completed, 0 failures: 30 success, 3 skipped (Test (coverage), Test (coverage shard …), dependabot). Type Check ✓ · Test shards 1–4 ✓ · Lint ✓ · Bundle Analysis ✓ (types (strict-authoring-face.js) 14.27 KB / 5.47 KB gz; console eager closure 3477.5 KB under 3512.7 KB) · Changeset Bump Policy / Declaration / Fixed Group / Overwrite ✓ · Governed Surface Queue Guard ✓ · README Export Check ✓ · Build Docs ✓ (skip path, 12 s). mergeable_state unknown at read (draft; base origin/main has moved to f0e72e26, 7 commits ahead of merge-base c4326fe).
Maintainer-only merge: no — non-governed additive export under a maintainer-ruled programme; every open item above is implementer-floor or PM-seat process.
Side effects of this review, disclosed: none on any checkout. The head was read from a throwaway ref (deleted after posting); the build ran on a git archive extraction in scratch with the sibling objectui-7058-main install borrowed read-only via symlinks — vitest touched that install's ignored node_modules/.vite/vitest cache and left .vite-temp empty; its tracked tree is clean.
Generated by Claude Code
Part of #8345 — the strict face itself, under the #5250 ruling (director comment
5534418546, maintainer 2026-09-04, decision batch #25, option 2). ⛔ No consumer is wired:objectui validate, the JSON-fence gate andobjectui checkare devx's half and are ruled to come after.Attribution, in prose because a body edit downgrades the footer: written by Claude Code, seat session
session_01Jmxdo7bmeqCQHLSfmLVX9w.Notation. Generic parameters and placeholders are written as UPPERCASE words rather than in their real angle-bracket spelling, because GitHub deletes tag-shaped fragments on save — backticks and fences included.
Patch round at
8e9b563— the contract review returned FAIL, and it was rightThe ceiling-tier review (verdict
5591195581) found a real defect underneath every passing pin. It is fixed here, and the two prerequisites it named are now pinned. The fresh whole-change review of this head (5592039737) returned PASS.The defect
The walker's type guard was
typeof value === 'object'. Zod 4.4.3 builds some objects through$ZodObjectJIT, and those instances are CALLABLE — they answertypeof 'function', their constructor prints as a boundZodObject, their traits readZodObject/$ZodObjectJIT/$ZodObject/$ZodType, and they parse like any other object. 20 of them are reachable on this face, all through@objectstack/spec-derived subtrees. The guard handed each one straight back, so the whole subtree beneath went unwalked.Measured on the built
dist, before and after, by the same probe:ad99bee8e9b563page.interfaceConfig.sort[]unrecognized_keysnamesinventedDeepKeyThe six that were open:
page.interfaceConfig.sort[],page.slots.header[0].in.visibleWhen[1]and its.meta,page.slots.header[0].in.dataSource.filter(lazy).right(all zod strip mode), andlist-view.bulkActionDefs[].params[]with…params[].options[](bothcatchall: unknown, i.e. passthrough).⛔ The claim was not weakened to match the walker. The walker was fixed so the published sentences — in the changeset, the README and this body — became true. All three are unchanged.
⭐ Why nothing here could have caught it, and the pin that now does
Two instruments shared the defect with the thing they were measuring.
typeof node !== 'object'— the identical blind spot. That is why "39 closed, identical before and after" read clean.⇒ The new pin is over the population, not over a sample document: walk the forced twin and require every object in it to carry
catchall: never. Its non-vacuity control is the one whose absence let this through —functionTypedmust be greater than zero, so a census that cannot see callable nodes fails loudly instead of reporting a graph it never entered.ad99bee. There the population assertion goes green with nothing behind it, and the callable-node control is the only thing that fires. That leg is the proof the round-1 gap is closed, and I had described my own pin as slightly stronger than it is.The cycle invariant, now pinned
The review measured the module cycle under four bundlers. Node, Vite/rolldown and Next 16.3.1 Turbopack are green in both entry orders. rollup 4.62.2, deep-module-first with a namespace import used as a value, is RED —
ReferenceError: Cannot access 'StrictAnyComponentSchema' before initialization, from the namespace object rollup synthesizes ahead of the deep module's body; re-confirmed still red at this head. Named imports in the same order are green; barrel-first is green.Unreachable today, and that is the point: my stated reason for safety — "read only inside the lazy getters" — is true and not sufficient. The load-bearing invariant is the barrel is the sole entry into the cycle, and it now has a pin covering both routes a caller has:
exportsmap has no wildcard subpath and no entry reaching the deep module (so nothing outside the package can deep-link it);packages,apps,examples,scripts,e2e, matched on import specifiers so thatscripts/measure-strict-authoring-face.mjsdoes not read as an importer merely by sharing the words in its own name.@object-ui/typestopackages/types/srcby prefix, so inside this repo a deep specifier resolves even though theexportsmap blocks it for consumers. The repo scan is the half that covers that route; the manifest assertion alone would not.The opaque-limit census — four numbers, and which one is the face
8e9b563with an every-def-key walk written for this purpose rather than reusing the shipped walker:onOpaqueShapeover the eager walk alone; everyz.lazystill deferredcustom 70 · function 4 · transform 4= 78custom 71 · function 4 · transform 4= 79custom 72 · function 4 · transform 4= 80defkey, not a fixed listcustom 74 · function 4 · transform 4= 82⇒ 80 is the census of the face as the walker sees it, and it agrees exactly with the review's independent reading.
⭐ The last row is worth keeping rather than rounding away. The two extra
customnodes are reached only throughdef.checks— they live inside a refinement, not in a schema's shape. The walker never traverseschecks, deliberately: checks are carried across whole by the def-patching clone rather than rebuilt, which is the property the refinement pins exist for. SoonOpaqueShape's contract is precisely "every unclosed shape the walker meets", ⛔ not "every opaque node on the face" — a distinction with no accept-set consequence (a preserved check still runs) but one this body should state rather than let a reader infer the stronger claim.Pipes. Four reachable, not one:
transformintoenum(a preprocessor underpage.interfaceConfig.filterBy[]), plusobject,stringandarrayeach into atransform. The walker's own comment said "one pipe" — a reading taken through the blind guard — and commit8e9b563rewrites it. No OBJECT sits on anoutside today, so walking both sides still moves no accept set; it is there so the first preprocessor wrapping an object does not open a hole.What lands
Three values and two types, published from
@object-ui/types/zod:StrictAnyComponentSchemaAnyComponentSchemaStrictSchemaNodeSchemaSchemaNodeSchemaderiveStrictAuthoringSchema(schema, options)StrictAuthoringLimit,DeriveStrictAuthoringOptionsonOpaqueShapereportDerived, never hand-written. Every reachable object is closed through unions, discriminated unions, arrays, tuples, records, intersections, optionals, nullables, defaults, both sides of a pipe,
z.lazy, and — since the patch round — callable JIT nodes. Objects are cloned by patching a copy of their own def and calling their own constructor — ⛔ never rebuilt with a fresh object literal, which dropsdef.checksand would make the twin under-report red. A callable node clones through its own bound constructor and comes back object-typed: a difference in representation, not in behaviour, and the population pin measures behaviour.⛔ The rendering face's passthrough is not flipped. This adds a face; it does not change the existing one — pinned, not promised.
All at
8e9b563, zod 4.4.3, corpus 556 documents / 2099 nodes (catalog + docs + authored). The card's 176/553 and 184/2099 are pinned at5505aec1and are not these numbers.c4326fe(base)typeBase and head are identical in every cell — the corpus-scale statement that this PR moves nothing about the existing face.⚠️ And the same table was identical before the guard fix, which is exactly why it cannot stand as evidence that the face is closed.
⭐ Agreement with the prototype, with the script's own whole-tree twin swapped for the shipped
StrictAnyComponentSchema(scratch copy, never committed): 174 / 556, red-today control 46 — identical.The blocker's property, re-read on this tree
The old 294/553 blow-up is gone: whole-tree strict (174) now sits beside per-node strict (179 of 2099) instead of an order of magnitude above it, because since #8344 a child slot resolves to the component union rather than the base keys. That is the error #7935 existed to prevent.
Where the module lives — ruled A by the seat (
5590686191), not reopened__tests__/zod-mirror-parity.test.tsruns a census closed over theexport consts ofsrc/zod/*.zod.ts, and that file is fenced this round (PR #8553). So the derivation module lives atpackages/types/src/strict-authoring-face.ts, outside the mirror directory, and the barrel re-exports from it. The collision is measured, not predicted — ablation leg 1 below. B (move it in, add anEXCLUSIONSrow) is recorded as a follow-up for after #8553 lands.The pins
packages/types/src/__tests__/strict-authoring-face-8345.test.ts— 25 tests.cardwithbuttonandtextchildren parses under the strict face, and under the tolerant one.unrecognized_keysnaming exactly that key; the same document accepted by the tolerant face.catchall: never; plus the callable-node control; plus a discriminator control showing the tolerant face is not closed.variant/sizeare ACCEPTED.chatbotbody clause still refuses a recordbodyone slot down while the root form is accepted, on both faces.outside of a pipe.tsc -p tsconfig.test.jsonand by nothing else — vitest does not typecheck. Non-vacuity with--listFiles: the test program lists the pin file (1 hit), the emitting program does not (0), both list the source.Ablations — each proved on disk, each restored under a trap
Every leg: blob hash before and after (a byte-identical mutation aborts the leg as a no-op), an
EXIT INT TERMtrap with absolute paths, and a restore verified by the hash returning to the HEAD blob andgit diff HEADbeing empty. Every leg printed its RESTORED-OK line.export constappended tozod/index.zod.tsindex.zod.ts#__StrictAuthoringCensusProbeoutwalkoutbranch deletedtsc -p tsconfig.test.jsonexpected [ …(6) ] to deeply equal [], and REPRO-Aad99beepackages/core/srcimporting the deep modulegit status --porcelainemptyGates
All at
8e9b563; each exit code captured before any pipe.pnpm exec vitest run packages/types/pnpm --filter @object-ui/types type-check(all three programs)pnpm --filter @object-ui/types lintpnpm --filter @object-ui/types buildcheck:control-bytes·check:published-tsconfig-exclude·check:side-effects-array·check:esm-specifiers·check:self-importcheck:dist-completeness·check:published-distcheck:doc-fences·check:doc-snippets·check:doc-types·check:doc-examplescheck:governed-queue-guard--teston all five changed paths: NOT GOVERNEDnode scripts/check-changeset-presence.mjsgrep -naPcontrol-byte self-scan of the changed filescheck:node-esm-load, plaincheck:node-esm-load --force-build37 of 37 … built by this tree,34 of 39 … imported and evaluated, 5 by design.turbo/cacheacross every worktree of this checkout, so the plain run refuses two entries it did not build, neither of them in this diff, while@object-ui/typesis built by this tree in both runs. But my sentence "on CI the refusal cannot arise" implied a coverage CI does not provide.node-esm-load-gate.ymlruns on a nightly cron and on push tomain; onlycheck:esm-specifiersruns per PR. ⇒ the load leg is unmeasured by anyone on a PR head — ⛔ not "left to CI".unlabeledevent by a named account, on the card and the PR two seconds apart — a dual clear, which is the shape of a deliberate clear rather than a whole-set overwrite side effect. The correct instrument was one call away. Carrier state is the PM seat's to hold; ⛔ I do not touch labels.Not measured, stated rather than implied: the repo-wide
turbo run lintand the fullpnpm testbeyond CI's green run on this head; esbuild/tsup bundling of the cycle; any browser or runtime rendering (nothing renders this face); import-time cost in a bundled app (the derivation is deferred behindz.lazy, so it is not paid unless the face is parsed).Scope
Changeset:
@object-ui/typesminor — a new published face on a published entry point; grade confirmed against AGENTS.md 238–240 and the precedents. Five files: the derivation module, the barrel re-export block, the pin file, the package README, the changeset. ⛔ No fenced file is touched: notpackages/types/package.json(the cycle pin reads it, never writes it), not the package's build configuration, notsrc/record-components.ts, not__tests__/zod-mirror-parity.test.ts.Falls off the back, as ruled: wiring any consumer; repairing any of the 174; the TypeScript authoring face (#7927); closing the opaque validators.
Clause ② carrier: held and cleared by the PM seat on the verified PASS for this head. ⛔ Not mine to set, clear or reason about here. This PR stays draft, ⛔ not enqueued, ⛔ no auto-merge, until that seat says otherwise.